/*
 *  Copyright (C) 2004 Cidero, Inc.
 *
 *  Permission is hereby granted to any person obtaining a copy of 
 *  this software to use, copy, modify, merge, publish, and distribute
 *  the software for any non-commercial purpose, subject to the
 *  following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included
 *  in all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 *  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY IN CONNECTION WITH THE SOFTWARE.
 * 
 *  File: $RCSfile: HTTPProxyServer.java,v $
 *
 */

package com.cidero.server;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Vector;
import java.util.logging.Logger;

import com.cidero.http.*;
import com.cidero.util.ShoutcastOutputStream;
import com.cidero.util.MrUtil;

import com.cidero.upnp.UPnPException;

/**
 * HTTP server proxy class. Reads a single input stream from another HTTP
 * server and writes the data to one or more requesting devices. Has some
 * logic to (attempt to) synchronize devices of the same type.
 *
 */
public class HTTPProxyServer implements HTTPRequestListener
{
  private static Logger logger = Logger.getLogger("com.cidero.server");

  HTTPServerList serverList = null;

  // List of active sessions for this proxy server. Use vector since 
  // it's synchronized
  Vector sessionList = new Vector();
  
  int syncWaitMillisec;

  /**
   *  Constructor for the proxy. An HTTP server is started up on
   *  the specified port, and left running for the duration of the
   *  process  
   *
   *  @param  port  Port number of the proxy server
   *
   *  @param  syncWaitMillisec  
   *          Number of milliseconds to wait for multiple devices to join 
   *          synchronized device group.  0 to disable sync
   */
  public HTTPProxyServer( int port, int syncWaitMillisec )
    throws IOException
  {
    logger.fine("HTTPProxyServer: Opening server on port: " + port +
                " syncWaitMillisec: " + syncWaitMillisec );

    this.syncWaitMillisec = syncWaitMillisec;

    //
    // Instantiate a set of HTTP servers, one per network interface on
    // this machine. All the servers share the request listener in this
    // class
    //
    serverList = new HTTPServerList();
    if( serverList.open( port ) == false )
    {
      logger.severe("HTTPProxyServer: Error opening server on port: " + port );
      System.exit(-1);
    }

    serverList.addRequestListener( this );
    logger.fine("HTTPProxyServer: starting server " );

    serverList.start();
  }
  
  /**
   * Do clean shutdown of proxy server. This was added to clean up
   * after Soundbridge 'sketch' logic (make sure sketch mode is exited
   * on all listening soundbridges when server is exited)
   */
  public void close()
  {
    HTTPProxySession session;

    for( int n = 0 ; n < sessionList.size() ; n++ )
    {
      session = (HTTPProxySession)sessionList.get(n);
      session.close();
    }
  }
  

  /**
   *  Process HTTP GET request from media playback device. The GET request
   *  has encoded in it the name of the 'true' http address. For example,
   *  a UPnP server making use of this proxy will translate the URL
   * 
   *    http://64.236.34.196:80/stream/2001   (a typical internet radio addr) 
   *
   *    http://<proxyIP>:<proxyPort>/64.236.34.196:80/stream/2001
   *  
   *  TODO: May need to substitute different char for ':' in the original
   *  address. I have seen some devices get confused when parsing URL's 
   *  with more than one ':'
   *
   *  @return  Returns true if server should close connection on return,
   *           false otherwise (to allow socket to continue to be used by
   *           another thread)
   */
  public boolean httpRequestReceived( HTTPRequest httpReq )
  {
    logger.fine( "HTTPServer: httpRequestReceived" );
    logger.fine( httpReq.toString() );

		if( ! httpReq.isGetRequest() )
    {
      logger.warning( "Unsupported request type" );
      httpReq.returnBadRequest();
      return true;  
    }

    String urlPath = httpReq.getResource();
    
    logger.fine("HTTPServer: Get Request, URLPath = " + urlPath );
     
    //
    // Translate from proxy URL to 'real' URL (including 'http://' portion)
    //
    
    String realURL = "http:/" + urlPath;
    
    logger.fine("HTTPServer: realURL = " + realURL );

    //
    // Check to see if this URL is already being served to another device
    // of the same type via an existing HTTP proxy session. If so, join 
    // the session group. If not, start a new session. getSession method
    // is synchronized since HTTP streaming requests may arrive simultanously
    //

    HTTPProxySession session = null;

    String userAgent = httpReq.getHeaderValue( HTTP.USER_AGENT );

    logger.fine("HTTPServer: userAgent = " + userAgent );
    String host = httpReq.getHeaderValue( HTTP.HOST );
    //if( host != null )
    //      logger.info("HTTPServer: host = " + host );

    try 
    {
      if( syncSupportedForDevice( userAgent ) )
      {
        session = getSession( realURL, userAgent, syncWaitMillisec );
      }
      else
      {
        session = createSession( realURL, userAgent, 0 );
      }
    }
    catch( UPnPException e )
    {
      logger.warning( "Error starting proxy session" );
      httpReq.returnBadRequest();
      return true;
    }
    
    //
    // Check to see if client is requesting shoutcast-style metadata
    // Note: Audiotron doesn't send the Icy-Metadata header as far as
    // I can tell, but does send an 'Accept: */*'
    //
    boolean shoutcastMode = false;
    //boolean shoutcastMode = true;
    
    HTTPHeader hdr = httpReq.getHeader( "Icy-Metadata" );
    if( hdr != null )
    {
      logger.fine( "HTTPServer: Found shoutcast header");
      if( hdr.getValue().equals("1") )
      {
        logger.fine( "HTTPServer: Enabling shoutcast mode");
        //session.setIcyMetadataRequestFlag( true );
        shoutcastMode = true;
      }
    }
    else
    {
      logger.fine( "HTTPServer: No shoutcast header (Icy-MetaData) found");
    }
    
    HTTPConnection connection = httpReq.getConnection();

    logger.fine( "SENDBUFSIZE = " + connection.getSendBufferSize() +
                 "RXBUFSIZE = " + connection.getReceiveBufferSize() );

    connection.setTcpNoDelay( true );

    // 
    // Send HTTP response header back to client
    //

		HTTPResponse httpResponse = new HTTPResponse();
		httpResponse.setStatusCode( HTTPStatus.OK );
    //httpResponse.setContentType( "audio/mpeg" );
    httpResponse.setContentType( "unknown/unknown" );
    httpResponse.setContentLength( -1 );
    
    //shoutcastMode = false;  // For sync testing with windows media player
    
    if( shoutcastMode )
    {
      // Set things up to insert metadata every 8192 bytes (~0.5 sec)
      // It only gets inserted when the song title changes...
      logger.fine( "HTTPServer: setting icy-metaint header to 8192");
      httpResponse.addHeader( "icy-metaint", "8192" );
      httpResponse.addHeader( "icy-name", "IcyName" );
    }

    logger.fine("HTTPServer Response Header:\n" + httpResponse.toString() );
    logger.fine("HTTPServer: Posting Header");

    try
    {
      connection.sendResponseHeader( httpResponse );
    }
    catch( IOException e )
    {
      logger.warning("HTTPServer: Error sending response header");
      return true;
    }
    
    //
    // Get the underlying stream from the HTTP socket and wrap it 
    // in a ShoutcastOutput stream
    //
    ShoutcastOutputStream outStream;
    if( shoutcastMode )
    {
      logger.info("Opening shoutcast output stream (shoutcast period = 8192)");
      outStream = new ShoutcastOutputStream( connection.getOutputStream(),
                                             8192 );
    }
    else
    {
      logger.info("Opening shoutcast output stream (no shoutcast data)");
      outStream = new ShoutcastOutputStream( connection.getOutputStream(),
                                             0 );  // disabled shoutcast mode
    }
    
    logger.fine("Joining sync shoutcast group ");

    //
    // Join the synchronous shoutcast group for the proxy session
    // synchronous shoutcast group and run it in current thread.
    // Subsequent requests are handled by adding streams to the
    // group.  Routine only returns a valid group for the first 
    // thread that joins it, otherwise null. Subsequent threads are 
    // 'joined' with first thread
    //
    SyncShoutcastGroup syncShoutGroup = 
      session.joinSyncShoutcastGroup( outStream, connection );

    if( syncShoutGroup == null )
    {
      // Joined existing session - return false to terminate this thread while
      // leaving socket open
      return false;  
    }
    
    //
    // 1st client in session - start it up.  Wait a bit for other clients
    // to join before proceeding.  
    //
    //

    logger.fine("Running sync shoutcast group ");

    if( session.getSyncWaitMillisec() > 0 )
    {
      logger.info(" Delaying " + 
                  session.getSyncWaitMillisec()/1000 +
                  " seconds waiting for other renderers to join session");
      MrUtil.sleep( session.getSyncWaitMillisec() - 1000 );
    }
    
    logger.info(" Starting session");
    session.start();

    // Give the session a bit of time to get going before starting sync group
    // reading the proxy queue - TODO - Necessary??
    if( session.getSyncWaitMillisec() > 0 )   
      MrUtil.sleep(1000);
    
    // The run routine returns when end of input from master server
    // detected, or if *all* clients disconnected
    syncShoutGroup.run();

    removeSession( session );

    return true;
	}

  /**
   * Certain devices have behavior that makes it hard to synchronize them.
   * Know problem devices hardcoded here.
   * 
   * Default is to assume that sync *will* work ok.  (return true)
   *
   */ 
  public boolean syncSupportedForDevice( String userAgent )
  {
    if( userAgent != null )
    {
      // DLink DSM-320  ('redsonic' in user-agent string) has puzzling
      // startup behavior when connecting with Cidero HTTP server. It
      // makes multiple requests in quick succession, specifying a 
      // range header that increments each time. Sort of like it
      // reads a little bit but times out too fast early on. It seems
      // to settle down after the 3rd HTTP request and just work. Maybe
      // it's trying to read MP3 header info at the front of the file,
      // and it's missing for radio streams?  For now, just disable 
      // sync for it.  
      //
      // Note: the 3rd request seems to always have a 'range' header of 
      // 'Range: bytes=49152-'. If desparate to make the Dlink sync-able,
      // allowing sync for the 3rd request may work ok.
      // 
      if( userAgent.toLowerCase().indexOf("redsonic") >= 0 )
        return false;
    }
    
    return true;
  }

  /**
   * Create a new proxy session
   *
   * @param  syncWaitMillisec
   *         Number of seconds to wait for multiple devices to join 
   *         synchronized device group.  0 to disable sync
   */     
  public synchronized HTTPProxySession createSession( String realURL,
                                                      String userAgent,
                                                      int syncWaitMillisec )
    throws UPnPException
  {
    HTTPProxySession session = new HTTPProxySession( realURL, userAgent,
                                                     syncWaitMillisec );
    sessionList.add( session );
    return session;
  }

  /**
   * Search for recently created proxy session with matching URL and
   * HTTP userAgent header (Assume same userAgent = same device type for now).
   * Create new session if match not found.
   * 
   */
  public synchronized HTTPProxySession getSession( String realURL,
                                                   String userAgent,
                                                   int syncWaitMillisec )
    throws UPnPException
  {
    HTTPProxySession session = null;

    if( syncWaitMillisec > 0 )
      session = findSession( realURL, userAgent, syncWaitMillisec );

    if( session == null )
    {
      logger.fine( "No synchronous session found - creating new one" );
      session = new HTTPProxySession( realURL, userAgent, syncWaitMillisec );
      sessionList.add( session );
    }
    else
    {
      logger.fine("HTTPProxyServer: Found matching session");
    }

    return session;
  }
    
  /**
   *  Find a session with a given resourceURL
   *
   *  @param   resourceURL
   *           Name of the resource on the UPnP server
   *
   *  @param   userAgent
   *           UserAgent of the HTTP client. This must match in order
   *           for session sharing to work (same device type)
   *
   *  @return  session or null if no session with specified URL pair exists
   *
   *  TODO: possible race condition between joining session and session
   *        starting up without the currently joining renderer - add lock
   */
  public synchronized HTTPProxySession findSession( String resourceURL,
                                                    String userAgent,
                                                    long syncWaitMillisec )
  {
    logger.fine("Searching for session with resource " + 
                resourceURL );

    HTTPProxySession session = null;

    for( int n = 0 ; n < sessionList.size() ; n++ )
    {
      session = (HTTPProxySession)sessionList.get(n);

      if( session.getResourceURL().equals( resourceURL ) &&
          session.getUserAgent().equals( userAgent ) )
      {
        long currTime = System.currentTimeMillis();
        if( (currTime - session.getCreateTimeMillis()) < syncWaitMillisec ) 
          return session;
      }
    }
    
    logger.fine("No matching session found");

    return null;
  }

  public synchronized void removeSession( HTTPProxySession session )
  {
    sessionList.remove(session);
  }

  

  /**
   * Get number of active sessions. This is one of the reported fields 
   * in the Web interface
   */
  public int getNumActiveSessions()
  {
    System.out.println("getNumSess");
    
    return sessionList.size();
  }


}


